package model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Pattern;
import model.commands.Command;
import model.factories.CommandFactory;
import model.factories.booleans.AndCommandFactory;
import model.factories.booleans.EqualCommandFactory;
import model.factories.booleans.GreaterThanCommandFactory;
import model.factories.booleans.LessThanCommandFactory;
import model.factories.booleans.NotCommandFactory;
import model.factories.booleans.NotEqualCommandFactory;
import model.factories.booleans.OrCommandFactory;
import model.factories.commandable.BackwardCommandFactory;
import model.factories.commandable.ClearScreenCommandFactory;
import model.factories.commandable.ForwardCommandFactory;
import model.factories.commandable.HideTurtleCommandFactory;
import model.factories.commandable.HomeCommandFactory;
import model.factories.commandable.LeftCommandFactory;
import model.factories.commandable.SetPenDownCommandFactory;
import model.factories.commandable.SetPenUpCommandFactory;
import model.factories.commandable.RightCommandFactory;
import model.factories.commandable.SetHeadingCommandFactory;
import model.factories.commandable.SetXYCommandFactory;
import model.factories.commandable.ShowTurtleCommandFactory;
import model.factories.commandable.TowardsCommandFactory;
import model.factories.controlflow.ListCommandFactory;
import model.factories.controlflow.ForCommandFactory;
import model.factories.controlflow.IfCommandFactory;
import model.factories.controlflow.IfElseCommandFactory;
import model.factories.environment.ChangeBackgroundImageCommandFactory;
import model.factories.environment.ClearStampsCommandFactory;
import model.factories.environment.CreateTurtleCommandFactory;
import model.factories.environment.PenColorCommandFactory;
import model.factories.environment.SetBackgroundCommandFactory;
import model.factories.environment.SetGridSizeCommandFactory;
import model.factories.environment.SetLanguageCommandFactory;
import model.factories.environment.SetPaletteCommandFactory;
import model.factories.environment.SetPenColorCommandFactory;
import model.factories.environment.SetPenDesignCommandFactory;
import model.factories.environment.SetPenSizeCommandFactory;
import model.factories.environment.SetShapeCommandFactory;
import model.factories.environment.SetShapeIndexCommandFactory;
import model.factories.environment.StampCommandFactory;
import model.factories.environment.TellCommandFactory;
import model.factories.environment.TellEvenCommandFactory;
import model.factories.environment.TellOddCommandFactory;
import model.factories.environment.ToggleGridCommandFactory;
import model.factories.environment.ToggleHighlightCommandFactory;
import model.factories.math.DifferenceCommandFactory;
import model.factories.math.MinusCommandFactory;
import model.factories.math.ProductCommandFactory;
import model.factories.math.QuotientCommandFactory;
import model.factories.math.RandomCommandFactory;
import model.factories.math.RemainderCommandFactory;
import model.factories.math.SumCommandFactory;
import model.factories.misc.LiteralCommandFactory;
import model.factories.misc.LoadCommandFactory;
import model.factories.misc.MakeCommandFactory;
import model.factories.misc.ParenthesisCommandFactory;
import model.factories.misc.SaveCommandFactory;
import model.factories.misc.ToCommandFactory;
import model.factories.misc.UserDefinedCommandFactory;
import model.factories.misc.VariableCommandFactory;
import model.factories.turtlequeries.HeadingCommandFactory;
import model.factories.turtlequeries.PenDownCommandFactory;
import model.factories.turtlequeries.ShowingCommandFactory;
import model.factories.turtlequeries.XCorCommandFactory;
import model.factories.turtlequeries.YCorCommandFactory;


/**
 * This class is responsible for reading in an input stream of commands as
 * strings, parsing them, creating appropriate command node objects, and then
 * executing them.
 * 
 * @author JLongley
 * @author Erick Gonzalez
 */

public class Interpreter {

    private static final String DEFAULT_RESOURCE_PACKAGE = "resources.";

    private Environment myEnvironment;
    private Map<String, Integer> myGlobalVariables;
    private Map<String, String> myUserDefinedCommands;
    private ResourceBundle myLanguage;
    private List<String> myCommandHistory;
    private List<CommandFactory> myCommandFactories;

    /**
     * 
     * @param environment an Environment object
     */
    public Interpreter (Environment environment) {
        myEnvironment = environment;
        myGlobalVariables = new HashMap<String, Integer>();
        myUserDefinedCommands = new HashMap<String, String>();
        try {
            setLanguage("English");
        }
        catch (SyntaxErrorException e) {
            e.printStackTrace();
        }
        myCommandHistory = new ArrayList<String>();
        initializeFactories();
    }

    private void initializeFactories () {
        myCommandFactories = new ArrayList<CommandFactory>();
        
        initializeCommandableFactories();
        initializeTurtleQueryFactories();
        initializeMathFactories(); 
        initializeBooleanFactories();      
        initializeControlFlowFactories();
        initializeEnvironmentFactories(); 
        initializeMiscFactories();                        
    }

    private void initializeMiscFactories () {
        myCommandFactories.add(new LiteralCommandFactory(this));
        myCommandFactories.add(new ListCommandFactory(this));
        myCommandFactories.add(new MakeCommandFactory(this));
        myCommandFactories.add(new VariableCommandFactory(this));
        myCommandFactories.add(new UserDefinedCommandFactory(this));
        myCommandFactories.add(new ToCommandFactory(this));        
        myCommandFactories.add(new SaveCommandFactory(this));
        myCommandFactories.add(new LoadCommandFactory(this));        
        myCommandFactories.add(new ParenthesisCommandFactory(this));
    }

    private void initializeEnvironmentFactories () {
        myCommandFactories.add(new ChangeBackgroundImageCommandFactory(this));
        myCommandFactories.add(new SetBackgroundCommandFactory(this));
        myCommandFactories.add(new SetPaletteCommandFactory(this));
        myCommandFactories.add(new CreateTurtleCommandFactory(this));
        myCommandFactories.add(new StampCommandFactory(this));
        myCommandFactories.add(new ClearStampsCommandFactory(this));
        myCommandFactories.add(new SetPenColorCommandFactory(this));
        myCommandFactories.add(new SetPenSizeCommandFactory(this));
        myCommandFactories.add(new TellCommandFactory(this));
        myCommandFactories.add(new PenColorCommandFactory(this));
        myCommandFactories.add(new TellEvenCommandFactory(this));
        myCommandFactories.add(new TellOddCommandFactory(this));
        myCommandFactories.add(new SetPenDesignCommandFactory(this));        
        myCommandFactories.add(new SetGridSizeCommandFactory(this));
        myCommandFactories.add(new ToggleGridCommandFactory(this));
        myCommandFactories.add(new ShowingCommandFactory(this));
        myCommandFactories.add(new SetShapeCommandFactory(this));
        myCommandFactories.add(new SetShapeIndexCommandFactory(this));
        myCommandFactories.add(new SetLanguageCommandFactory(this));
        myCommandFactories.add(new ToggleHighlightCommandFactory(this));
    }

    private void initializeControlFlowFactories () {
        myCommandFactories.add(new IfCommandFactory(this));
        myCommandFactories.add(new IfElseCommandFactory(this));        
        myCommandFactories.add(new ForCommandFactory(this));
    }

    private void initializeBooleanFactories () {
        myCommandFactories.add(new LessThanCommandFactory(this));
        myCommandFactories.add(new GreaterThanCommandFactory(this));
        myCommandFactories.add(new EqualCommandFactory(this));
        myCommandFactories.add(new NotEqualCommandFactory(this));
        myCommandFactories.add(new AndCommandFactory(this));
        myCommandFactories.add(new OrCommandFactory(this));
        myCommandFactories.add(new NotCommandFactory(this));
    }

    private void initializeMathFactories () {
        myCommandFactories.add(new SumCommandFactory(this));        
        myCommandFactories.add(new DifferenceCommandFactory(this));
        myCommandFactories.add(new ProductCommandFactory(this));
        myCommandFactories.add(new QuotientCommandFactory(this));
        myCommandFactories.add(new RemainderCommandFactory(this));
        myCommandFactories.add(new MinusCommandFactory(this));
        myCommandFactories.add(new RandomCommandFactory(this));
    }

    private void initializeTurtleQueryFactories () {
        myCommandFactories.add(new XCorCommandFactory(this));
        myCommandFactories.add(new YCorCommandFactory(this));
        myCommandFactories.add(new HeadingCommandFactory(this));
        myCommandFactories.add(new PenDownCommandFactory(this));
    }

    private void initializeCommandableFactories () {
        myCommandFactories.add(new ForwardCommandFactory(this));
        myCommandFactories.add(new BackwardCommandFactory(this));
        myCommandFactories.add(new LeftCommandFactory(this));
        myCommandFactories.add(new RightCommandFactory(this));
        myCommandFactories.add(new SetHeadingCommandFactory(this));
        myCommandFactories.add(new TowardsCommandFactory(this));
        myCommandFactories.add(new SetXYCommandFactory(this));
        myCommandFactories.add(new SetPenUpCommandFactory(this));
        myCommandFactories.add(new SetPenDownCommandFactory(this));
        myCommandFactories.add(new ShowTurtleCommandFactory(this));
        myCommandFactories.add(new HideTurtleCommandFactory(this));
        myCommandFactories.add(new HomeCommandFactory(this));
        myCommandFactories.add(new ClearScreenCommandFactory(this));
    }

    /**
     * Creates command objects from the input stream and evaluates them
     * and then evaluates it.
     * 
     * @param commandReader a scanner that reads in commands
     * @return the return value of the final command
     * @throws SyntaxErrorException this catch-all will work for any unknown
     *         token.
     *         It will catch unknown functions and variables as well as an
     *         incorrect number of arguments
     */
    public int interpretAndExecute (Scanner commandReader) throws SyntaxErrorException {
        // 0 will never be returned, because an exception will be thrown
        int returnValue = 0;
        while (commandReader.hasNext()) {
            Command parsedCommand = parse(commandReader, myGlobalVariables);
            returnValue = parsedCommand.evaluate(myEnvironment);
        }
        return returnValue;
    }

    /**
     * Creates command objects from the input stream and evaluates them
     * and then evaluates it.
     * 
     * @param commands a string containing the commands
     * @return the return value of the final command
     * @throws SyntaxErrorException this catch-all will work for any unknown
     *         token. It will catch unknown functions and variables as well as an
     *         incorrect number of arguments
     */
    public int interpretAndExecute (String commands) throws SyntaxErrorException {
        try {
            int returnValue = interpretAndExecute(new Scanner(commands));
            myCommandHistory.add(commands);
            return returnValue;
        }
        catch (SyntaxErrorException e) {
            throw new SyntaxErrorException();
        }
    }

    /**
     * Our parser breaks a command sequence into a tree data structure and
     * executes
     * using a post order traversal of the tree.
     * 
     * @param commandReader a command reader
     * @param variables a mapping from variable name to variable value
     * @return the root Command object
     * @throws SyntaxErrorException this catch-all will work for any unknown
     *         token.
     *         It will catch unknown functions and variables as well as an
     *         incorrect number of arguments
     */
    public Command parse (Scanner commandReader,
                          Map<String, Integer> variables) throws SyntaxErrorException {
        for (CommandFactory commandFactory : myCommandFactories) {
            Pattern p = commandFactory.patternToMatch();
            
            if (commandReader.hasNext(p)) {
                return commandFactory.createCommand(commandReader, variables);
            }
        }
        // if next token doesn't match any known command
        throw new SyntaxErrorException("Unrecognized command!");
    }

    /**
     * 
     * @param commandName a user defined command name
     * @return a CommandInfo corresponding to this name
     */
    public String getCommandInfo (String commandName) {
        return myUserDefinedCommands.get(commandName);
    }

    /**
     * 
     * @return a set of all user defined command names
     */
    public Set<String> getUserDefinedCommandNames () {
        return myUserDefinedCommands.keySet();
    }

    /**
     * Associates a command name with its corresponding CommandInfo object.
     * 
     * @param commandName a user defined command name
     * @param commandInfoString a string representing user defined command info
     */
    public void makeUserDefinedCommand (String commandName,
                                        String commandInfoString) {
        myUserDefinedCommands.put(commandName, commandInfoString);
    }

    /**
     * 
     * @return an environment object belonging to this interpreter
     */
    public Environment getEnvironment () {
        return myEnvironment;
    }

    /**
     * 
     * @return the command history
     */
    public List<String> getCommandHistory () {
        return myCommandHistory;
    }

    /**
     * Clears the command history
     */
    public void clearCommandHistory () {
        myCommandHistory.clear();
    }
    
    /**
     * Gets the language used by the interpreter
     * @return a resourcebundle that stores the keywords of the language
     */
    public ResourceBundle getLanguage () {
        return myLanguage;
    }

    /**
     * Sets the language used by the interpreter
     * @param language the language to set the interpreter to
     * @throws SyntaxErrorException when language is not known
     */
    public void setLanguage (String language) throws SyntaxErrorException {
        try {
            this.myLanguage = ResourceBundle.getBundle(DEFAULT_RESOURCE_PACKAGE
                                                       + language);
        }
        catch (MissingResourceException e) {
            throw new SyntaxErrorException("Language not found!");
        }
    }
}
